msg_tool\scripts\cat_system/
cst.rs1use crate::ext::io::*;
3use crate::scripts::base::*;
4use crate::types::*;
5use crate::utils::encoding::*;
6use anyhow::Result;
7use fancy_regex::Regex;
8use int_enum::IntEnum;
9use std::io::{Read, Write};
10
11#[derive(Debug)]
12pub struct CstScriptBuilder {}
14
15impl CstScriptBuilder {
16 pub fn new() -> Self {
18 CstScriptBuilder {}
19 }
20}
21
22impl ScriptBuilder for CstScriptBuilder {
23 fn default_encoding(&self) -> Encoding {
24 Encoding::Cp932
25 }
26
27 fn build_script(
28 &self,
29 buf: Vec<u8>,
30 _filename: &str,
31 encoding: Encoding,
32 _archive_encoding: Encoding,
33 config: &ExtraConfig,
34 _archive: Option<&Box<dyn Script>>,
35 ) -> Result<Box<dyn Script>> {
36 Ok(Box::new(CstScript::new(buf, encoding, config)?))
37 }
38
39 fn extensions(&self) -> &'static [&'static str] {
40 &["cst"]
41 }
42
43 fn script_type(&self) -> &'static ScriptType {
44 &ScriptType::CatSystem
45 }
46
47 fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
48 if buf_len >= 8 && buf.starts_with(b"CatScene") {
49 return Some(255);
50 }
51 None
52 }
53}
54
55trait CustomFn {
56 fn write_patched_string(&mut self, s: &CstString, data: &[u8]) -> Result<usize>;
57}
58
59impl CustomFn for MemWriter {
60 fn write_patched_string(&mut self, s: &CstString, data: &[u8]) -> Result<usize> {
61 if data.len() + 1 > s.len {
62 let pos = self.data.len();
63 self.pos = pos;
64 self.write_u8(1)?; self.write_u8(u8::from(s.typ))?;
66 self.write_all(data)?;
67 self.write_u8(0)?; Ok(pos)
69 } else {
70 self.pos = s.address;
71 self.write_u8(1)?; self.write_u8(u8::from(s.typ))?;
73 self.write_all(data)?;
74 self.write_u8(0)?; Ok(s.address)
76 }
77 }
78}
79
80#[repr(u8)]
81#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, IntEnum)]
82enum CstStringType {
83 EmptyLine = 0x2,
84 Paragraph = 0x03,
85 Message = 0x20,
86 Character = 0x21,
87 Command = 0x30,
88 FileName = 0xF0,
89 LineNumber = 0xF1,
90}
91
92#[derive(Debug)]
93struct CstString {
94 typ: CstStringType,
95 text: String,
96 address: usize,
97 len: usize,
99}
100
101#[derive(Debug)]
102pub struct CstScript {
104 data: MemReader,
105 compressed: bool,
106 strings: Vec<CstString>,
107 compress_level: u32,
108}
109
110impl CstScript {
111 pub fn new(buf: Vec<u8>, encoding: Encoding, config: &ExtraConfig) -> Result<Self> {
117 let mut reader = MemReader::new(buf);
118 let mut magic = [0; 8];
119 reader.read_exact(&mut magic)?;
120 if &magic != b"CatScene" {
121 return Err(anyhow::anyhow!("Invalid CST script magic: {:?}", magic));
122 }
123 let compressed_size = reader.read_u32()?;
124 let uncompressed_size = reader.read_u32()?;
125 let mut file = if compressed_size == 0 {
126 if uncompressed_size != reader.data.len() as u32 - 0x10 {
127 return Err(anyhow::anyhow!(
128 "Uncompressed size mismatch: expected {}, got {}",
129 uncompressed_size,
130 reader.data.len() as u32 - 0x10
131 ));
132 }
133 MemReader::new((&reader.data[0x10..]).to_vec())
134 } else {
135 let mut decoder = flate2::read::ZlibDecoder::new(reader);
136 let mut data = Vec::with_capacity(uncompressed_size as usize);
137 decoder.read_to_end(&mut data)?;
138 MemReader::new(data)
139 };
140 let data_length = file.read_u32()?;
141 if data_length as usize + 0x10 != file.data.len() {
142 return Err(anyhow::anyhow!(
143 "Data length mismatch: expected {}, got {}",
144 data_length,
145 file.data.len() - 0x10
146 ));
147 }
148 let _clear_screen_count = file.read_u32()?;
149 let string_address_offset = 0x10 + file.read_u32()?;
150 let strings_offset = 0x10 + file.read_u32()?;
151 let string_count = (strings_offset - string_address_offset) / 4;
152 let mut strings = Vec::with_capacity(string_count as usize);
153 for i in 0..string_count {
154 let offset = file.cpeek_u32_at(string_address_offset as u64 + i as u64 * 4)? as usize
155 + strings_offset as usize;
156 file.pos = offset;
157 let start_marker = file.read_u8()?;
158 if start_marker != 1 {
159 return Err(anyhow::anyhow!(
160 "Invalid start marker for string {}: expected 0x01, got {:02X}",
161 i,
162 start_marker
163 ));
164 }
165 let typ = CstStringType::try_from(file.read_u8()?).map_err(|code| {
166 anyhow::anyhow!("Invalid string type for string {}: {:02X}", i, code)
167 })?;
168 let str = file.read_cstring()?;
169 let text = decode_to_string(encoding, str.as_bytes(), true)?;
170 strings.push(CstString {
171 typ,
172 text,
173 address: offset,
174 len: str.as_bytes_with_nul().len(),
175 });
176 }
177 Ok(CstScript {
178 data: file,
179 compressed: compressed_size != 0,
180 strings,
181 compress_level: config.zlib_compression_level,
182 })
183 }
184}
185
186lazy_static::lazy_static! {
187 static ref CST_COMMAND_REGEX: Regex = Regex::new(r"^\d+\s+\w+\s+(.+)").unwrap();
188}
189
190impl Script for CstScript {
191 fn default_output_script_type(&self) -> OutputScriptType {
192 OutputScriptType::Json
193 }
194
195 fn default_format_type(&self) -> FormatOptions {
196 FormatOptions::None
197 }
198
199 fn extract_messages(&self) -> Result<Vec<Message>> {
200 let mut messages = Vec::new();
201 let mut name = None;
202 for s in self.strings.iter() {
203 match s.typ {
204 CstStringType::Message => {
205 if s.text.is_empty() {
206 continue; }
208 messages.push(Message {
209 message: s.text.replace("\\n", "\n"),
210 name: name.take(),
211 });
212 }
213 CstStringType::Character => {
214 name = Some(s.text.clone());
215 }
216 CstStringType::Command => {
217 if let Some(caps) = CST_COMMAND_REGEX.captures(&s.text)? {
218 if let Some(text) = caps.get(1) {
219 messages.push(Message {
220 message: text.as_str().to_string(),
221 name: None,
222 });
223 }
224 }
225 }
226 _ => {}
227 }
228 }
229 Ok(messages)
230 }
231
232 fn import_messages<'a>(
233 &'a self,
234 messages: Vec<Message>,
235 mut file: Box<dyn WriteSeek + 'a>,
236 _filename: &str,
237 encoding: Encoding,
238 replacement: Option<&'a ReplacementTable>,
239 ) -> Result<()> {
240 let mut writer = MemWriter::from_vec(self.data.data.clone());
241 let mut mess = messages.iter();
242 let mut mes = mess.next();
243 let strings_address_offset = 0x10 + self.data.cpeek_u32_at(0x8)? as usize;
244 let strings_offset = 0x10 + self.data.cpeek_u32_at(0xC)? as usize;
245 let mut name_index = None;
246 for (i, s) in self.strings.iter().enumerate() {
247 match s.typ {
248 CstStringType::Message => {
249 if s.text.is_empty() {
250 continue; }
252 let m = match mes {
253 Some(m) => m,
254 None => {
255 return Err(anyhow::anyhow!("No enough messages."));
256 }
257 };
258 if let Some(name_idx) = name_index.take() {
259 let mut name = match &m.name {
260 Some(n) => n.clone(),
261 None => {
262 return Err(anyhow::anyhow!("Message has no name.",));
263 }
264 };
265 if let Some(replacement) = replacement {
266 for (k, v) in &replacement.map {
267 name = name.replace(k, v);
268 }
269 }
270 let data = encode_string(encoding, &name, true)?;
271 let s = &self.strings[name_idx];
272 let pos = writer.write_patched_string(s, &data)?;
273 if pos != s.address {
274 writer.write_u32_at(
275 strings_address_offset as u64 + name_idx as u64 * 4,
276 (pos - strings_offset) as u32,
277 )?;
278 }
279 }
280 let mut message = m.message.clone();
281 if let Some(replacement) = replacement {
282 for (k, v) in &replacement.map {
283 message = message.replace(k, v);
284 }
285 }
286 message = message.replace("\n", "\\n");
287 let data = encode_string(encoding, &message, true)?;
288 let pos = writer.write_patched_string(s, &data)?;
289 if pos != s.address {
290 writer.write_u32_at(
291 strings_address_offset as u64 + i as u64 * 4,
292 (pos - strings_offset) as u32,
293 )?;
294 }
295 mes = mess.next();
296 }
297 CstStringType::Character => {
298 name_index = Some(i);
299 }
300 CstStringType::Command => {
301 if let Some(caps) = CST_COMMAND_REGEX.captures(&s.text)? {
302 if let Some(mat) = caps.get(1) {
303 let m = match mes {
304 Some(m) => m,
305 None => {
306 return Err(anyhow::anyhow!("No enough messages."));
307 }
308 };
309 let mut text = m.message.clone();
310 if let Some(replacement) = replacement {
311 for (k, v) in &replacement.map {
312 text = text.replace(k, v);
313 }
314 }
315 let mut command_text = s.text.clone();
316 command_text.replace_range(mat.range(), &text);
317 let data = encode_string(encoding, &command_text, true)?;
318 let pos = writer.write_patched_string(s, &data)?;
319 if pos != s.address {
320 writer.write_u32_at(
321 strings_address_offset as u64 + i as u64 * 4,
322 (pos - strings_offset) as u32,
323 )?;
324 }
325 mes = mess.next();
326 }
327 }
328 }
329 _ => {}
330 }
331 }
332 if mes.is_some() || mess.next().is_some() {
333 return Err(anyhow::anyhow!("Not all messages were processed."));
334 }
335 let data_len = writer.data.len() as u32 - 0x10;
336 writer.write_u32_at(0, data_len)?;
337 let data = writer.into_inner();
338 file.write_all(b"CatScene")?;
339 file.write_u32(0)?; file.write_u32(data.len() as u32)?; if self.compressed {
342 let mut encoder = flate2::write::ZlibEncoder::new(
343 &mut file,
344 flate2::Compression::new(self.compress_level),
345 );
346 encoder.write_all(&data)?;
347 encoder.finish()?;
348 let file_len = file.stream_position()?;
349 let compressed_size = (file_len as u32) - 0x10;
350 file.write_u32_at(8, compressed_size)?;
351 } else {
352 file.write_all(&data)?;
353 }
354 Ok(())
355 }
356}